#include "page_stream.h"

#include <assert.h>
#include <stdlib.h>
#include <string.h>

#define PAGE_CACHE_NEWSIZE 2

static struct page *page_new(struct page_stream *s) {
  size_t newsize = s->pagesize + sizeof(struct page);

  struct page *page = (struct page *)malloc(newsize);
  if (page == NULL) return NULL;

  page->size = s->pagesize;
  list_init(&page->head);

  return page;
}

static void page_del(struct page_stream *s, struct page *page) {
  assert(s);
  free(page);
}

static struct page *page_cache_get(struct page_stream *s) {
  struct page *page;

  if (s->lrusize == 0) {
    for (int i = 0; i < PAGE_CACHE_NEWSIZE; i++) {
      page = page_new(s);
      assert(page);
      list_add_tail(&page->head, &s->lru);
      s->lrusize++;
    }
  }
  page = list_entry(s->lru.next, struct page, head);
  list_del(&page->head);
  s->lrusize--;

  return page;
}

static void page_cache_release(struct page_stream *s, struct page *page) {
  list_add_tail(&page->head, &s->lru);
  s->lrusize++;

  while (s->lrusize > (PAGE_CACHE_NEWSIZE << 1)) {
    page = list_entry(s->lru.next, struct page, head);
    list_del(&page->head);
    s->lrusize--;
    page_del(s, page);
  }
}

static size_t __page_stream_read(struct page_stream *s, void *ptr, size_t size,
                                 int nodrop) {
  size_t total = 0;
  unsigned char *lptr = (unsigned char *)ptr;
  struct page *current = NULL;
  struct list_head *head = s->head.next;
  size_t posread = s->pos_read;

  for (size_t canread = 0; size > 0; size -= canread, total += canread) {
    if (head->next == &s->head) break;

    current = list_entry(head, struct page, head);
    head = head->next;

    if (head == &s->head) {
      canread = s->pos_write - posread;
    } else {
      canread = current->size - posread;
    }

    canread = (size < canread) ? size : canread;
    if (lptr) {
      memcpy(lptr, current->data + s->pos_read, canread);
      lptr += canread;
    }
    posread += canread;

    if (posread >= current->size) {
      posread = 0;
      if (nodrop == 0) {
        list_del(&current->head);
        page_cache_release(s, current);
        if (list_is_empty(&s->head)) s->pos_write = 0;
      }
    }

    if (nodrop == 0) s->pos_read = posread;
  }

  return total;
}

size_t page_stream_drop(struct page_stream *s, size_t size) {
  return __page_stream_read(s, NULL, size, 1);
}

size_t page_stream_read(struct page_stream *s, void *ptr, size_t size) {
  return __page_stream_read(s, ptr, size, 1);
}

size_t page_stream_peek(struct page_stream *s, void *ptr, size_t size) {
  return __page_stream_read(s, ptr, size, 0);
}

size_t page_stream_write(struct page_stream *s, const void *ptr, size_t size) {
  size_t total = 0;
  size_t canwrite = 0;
  struct page *current = NULL;
  unsigned char *lptr = (unsigned char *)ptr;

  for (; size > 0; size -= canwrite, total += canwrite) {
    if (list_is_empty(&s->head)) {
      canwrite = 0;
      current = NULL;
    } else {
      current = list_entry(s->head.next, struct page, head);
      canwrite = current->size - s->pos_write;
    }
    if (canwrite == 0) {
      current = page_cache_get(s);
      assert(current);
      list_add_tail(&current->head, &s->head);
      s->pos_write = 0;
      canwrite = current->size;
    }

    canwrite = (size < canwrite) ? size : canwrite;
    memcpy(current->data + s->pos_write, lptr, canwrite);
    lptr += canwrite;
    s->pos_write += canwrite;
  }

  return total;
}
